package com.netbiscuits.transcoding;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.HttpCookie;
import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.validator.GenericValidator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.util.EntityUtils;
import org.apache.log4j.Logger;
import org.apache.struts.validator.LazyValidatorForm;

import com.netbiscuits.api.NameValue;
import com.netbiscuits.api.client.DetectionException;
import com.netbiscuits.api.client.DeviceDetection;
import com.netbiscuits.api.client.InternalResource;
import com.netbiscuits.api.client.InternalResourceException;
import com.netbiscuits.api.client.ProxyConfig;
import com.netbiscuits.api.client.TranscodeImage;
import com.netbiscuits.api.client.TranscodeImageException;
import com.netbiscuits.api.client.TranscodeMarkup;
import com.netbiscuits.api.client.TranscodingException;
import com.netbiscuits.api.http.OutputCaptureResponseWrapper;


public class NetbiscuitsMobileFilter implements Filter {

    private static final Logger LOG = Logger.getLogger(NetbiscuitsMobileFilter.class);
    private String proxyUrl = "";
    private String wsUser = "";
    private String wsPassword = "";
    private boolean enableDeviceDetection;
    private boolean enableDetectionFlow;
    private String[] deviceParamNames;
    private ServletContext context;
    private boolean debug = false;
    private String existingUAID;
    private String existingUserAgent;

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        HttpServletRequest httpReq = (HttpServletRequest) request;
        HttpServletResponse httpRes;
        if (response instanceof HttpServletResponse) {
            httpRes = (HttpServletResponse) response;
        } else {
            throw new ServletException("NetbiscuitsMobileFilter only accepts HTTP requests");
        }

        if (LOG.isDebugEnabled()) {
            @SuppressWarnings("unchecked")
            Enumeration<String> en = httpReq.getAttributeNames();
            LOG.debug("Attributes: ");
            while (en.hasMoreElements()) {
                String name = en.nextElement();
                LOG.debug(MessageFormat.format("{0} :: {1}", new Object[]{name, httpReq.getAttribute(name)}));
            }
            @SuppressWarnings("unchecked")
            Enumeration<String> enh = httpReq.getHeaderNames();
            LOG.debug("Headers: ");
            while (enh.hasMoreElements()) {
                String name = enh.nextElement();
                LOG.debug(MessageFormat.format("{0} :: {1}", new Object[]{name, httpReq.getHeader(name)}));
            }
            @SuppressWarnings("unchecked")
            Enumeration<String> enp = httpReq.getParameterNames();
            LOG.debug("Params: ");
            while (enp.hasMoreElements()) {
                String name = enp.nextElement();
                LOG.debug(MessageFormat.format("{0} :: {1}", new Object[]{name, httpReq.getParameter(name)}));
            }
        }

        ProxyConfig conf = new ProxyConfig(httpReq);
        conf.setProxyUrl(proxyUrl);
        StringBuilder portalUrl = new StringBuilder(httpReq.getScheme());
        portalUrl.append("://");
        portalUrl.append(GenericValidator.isBlankOrNull(httpReq.getHeader("x-forwarded-host")) ? httpReq.getHeader("host") : httpReq.getHeader("x-forwarded-host"));
        portalUrl.append(httpReq.getContextPath());
        conf.setPortalUrl(portalUrl.toString());
        if (LOG.isDebugEnabled()) {
            LOG.debug(MessageFormat.format("PortalUrl: {0}", conf.getPortalUrl()));
        }
        conf.setWsUser(wsUser);
        conf.setWsPassword(wsPassword);

        if (this.existingUAID != null) {
            conf.setExistingUAID(this.existingUAID);
        }
        if (this.existingUserAgent != null) {
            conf.setExistingUserAgent(this.existingUserAgent);
        }

        if (conf.getRelUri().startsWith("/img/")) { // convert images

            processTranscodeImage(conf, httpReq, httpRes);

        } else if (conf.getRelUri().startsWith("/nbinternal/")) { // get nbinternal resources

            processInternalResource(conf, httpReq, httpRes);

        } else if (conf.getRelUri().startsWith("/nbrif/")) { // get ajax calls

            OutputCaptureResponseWrapper resWrapper = new OutputCaptureResponseWrapper(httpRes);
            chain.doFilter(request, resWrapper);
            resWrapper.flushBuffer();

            String contentType = resWrapper.getContentType();
            if (contentType != null && contentType.indexOf("xml") != -1) { // trancode markup for content type xml

                String debugMarkup = request.getParameter("debugForceMarkup");
                if (debugMarkup != null && debugMarkup.length() > 0) {
                    httpRes.getOutputStream().write(resWrapper.getOutputAsByteArray());
                } else {
                    processTranscodeMarkup(conf, resWrapper.getOutputAsString(), httpRes, httpReq);
                }

            } else { // Let all other content types pass through.
                httpRes.getOutputStream().write(resWrapper.getOutputAsByteArray());
            }

        } else {

            OutputCaptureResponseWrapper resWrapper = new OutputCaptureResponseWrapper(httpRes);

            // This is an optimization to find out if we are in a detection workflow
            // without having to trigger a getDeviceParams call for each request
            String emvccCookie = getCookieValue(httpReq.getCookies(), "emvcc");
            String emvccParam = httpReq.getParameter("emvcc");
            boolean detectionWorkflow = false;

            if (emvccCookie == null && emvccParam == null) { // emvcc is not yet set
                detectionWorkflow = true;
            } else if (emvccCookie != null && emvccParam != null) { // remove emvcc from URL query string
                detectionWorkflow = true;
            }

            if (this.enableDetectionFlow && detectionWorkflow) {

                processTranscodeMarkup(conf,
                        "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><page><TEXT>detection workflow</TEXT></page>", httpRes, httpReq);

            } else { // pass the request through the filter chain

                // getDeviceParams can be disabled in netbiscuitsapi.properties
                // default is enabled
                if (this.enableDeviceDetection) {
                    processDeviceParams(conf, httpReq);
                }

                if (!this.enableDetectionFlow) {
                    NameValue[] options = new NameValue[2];
                    options[0] = new NameValue("disableJSDD", "true");
                    options[1] = new NameValue("disableCD", "true");
                    conf.setOptions(options);
                }

                chain.doFilter(request, resWrapper);
                resWrapper.flushBuffer();

                String contentType = resWrapper.getContentType();
                if (contentType != null && contentType.indexOf("xml") != -1) { // trancode markup for content type xml

                    String debugMarkup = request.getParameter("debugForceMarkup");
                    if (debugMarkup != null && debugMarkup.length() > 0) {
                        httpRes.getOutputStream().write(resWrapper.getOutputAsByteArray());
                    } else {
                        processTranscodeMarkup(conf, resWrapper.getOutputAsString(), httpRes, httpReq);
                    }

                } else { // Let all other content types pass through.
                    httpRes.getOutputStream().write(resWrapper.getOutputAsByteArray());
                }
            }
        }
    }

    public void init(FilterConfig config) throws ServletException {

        context = config.getServletContext();
        if (config.getInitParameter("debug") != null && config.getInitParameter("debug").contains("true")) {
            this.debug = true;
        }
        LOG.info("######################################## DEBUG: " + this.debug);

        Properties props = new Properties();
        String configFile = context.getRealPath("WEB-INF/netbiscuitsapi.properties");

        try {
            props.load(new FileInputStream(configFile));
            if (LOG.isDebugEnabled()) {
                LOG.debug("INIT");
                LOG.debug("proxyUrl: " + props.getProperty("proxyUrl"));
                LOG.debug("wsUser: " + props.getProperty("wsUser"));
                LOG.debug("wsPassword: " + props.getProperty("wsPassword"));
                LOG.debug("enableDeviceDetection: " + props.getProperty("enableDeviceDetection"));
                LOG.debug("enableDetectionFlow: " + props.getProperty("enableDetectionFlow"));
                LOG.debug("deviceParamNames: " + props.getProperty("deviceParamNames"));
                LOG.debug("existingUAID: " + props.getProperty("existingUAID"));
                LOG.debug("existingUserAgent: " + props.getProperty("existingUserAgent"));
            }
            this.proxyUrl = props.getProperty("proxyUrl");
            this.wsUser = props.getProperty("wsUser");
            this.wsPassword = props.getProperty("wsPassword");
            String devdet = props.getProperty("enableDeviceDetection", "true");
            if (devdet.contentEquals("true")) {
                this.enableDeviceDetection = true;
            } else {
                this.enableDeviceDetection = false;
            }
            LOG.debug("evaluated enableDeviceDetection: " + this.enableDeviceDetection);
            String detectflow = props.getProperty("enableDetectionFlow", "true");
            if (detectflow.contentEquals("true")) {
                this.enableDetectionFlow = true;
            } else {
                this.enableDetectionFlow = false;
            }
            LOG.debug("evaluated enableDetectionFlow: " + this.enableDetectionFlow);
            String devParams = props.getProperty("deviceParamNames", "ImageMaxWidth,ImageMaxHeight");
            if (!devParams.contains("UAID")) {
                if (devParams.indexOf(',') > 0) {
                    devParams += ",";
                }
                devParams += "UAID";
            }
            this.deviceParamNames = devParams.split(",");
            this.existingUAID = props.getProperty("existingUAID");
            this.existingUserAgent = props.getProperty("existingUserAgent");

        } catch (FileNotFoundException e) {
            LOG.error("INIT", e);
        } catch (IOException e) {
            LOG.error("INIT", e);
        }

    }

    public void destroy() {
        //work on clean up
    }

    private void processTranscodeMarkup(ProxyConfig config, String biscuitML, HttpServletResponse httpRes, HttpServletRequest httpReq)
            throws IOException {
        if (this.debug) {
            LOG.debug("processTranscodeMarkup...");
            LOG.debug(biscuitML);
        }
        try {

            TranscodeMarkup tm = new TranscodeMarkup(config, biscuitML);

            if (tm.getResultCode() == 0) {
                httpRes.setContentType(tm.getContentType());
                httpRes.setCharacterEncoding(tm.getCharacterEncoding());
                httpRes.setStatus(tm.getHttpResultCode());
                //ArrayList<Cookie> cookies = httpRes.getCookies();
                NameValue[] headers = tm.getResponseHeaders();
                if (headers != null) {
                    for (int i = 0; i < headers.length; i++) {
                        NameValue nv = headers[i];
                        // don't use setHeader to preserve existing cookies
                        if (nv.getName().equalsIgnoreCase("set-cookie")) {
                            String headerValue = nv.getValue();
                            List<HttpCookie> clist = HttpCookie.parse(headerValue);
                            for (HttpCookie httpck : clist) {
                                Cookie c = new Cookie(httpck.getName(), httpck.getValue());
                                c.setPath(httpck.getPath());
                                c.setMaxAge((int) httpck.getMaxAge());
                                c.setSecure(httpck.getSecure());
                                httpRes.addCookie(c);
                            }
                        } else if (nv.getName().equalsIgnoreCase("location")) {
                            String loc = nv.getValue();
                            if (loc.indexOf('?') > 0) {
                                String url = loc.substring(0, loc.indexOf('?'));
                                String query = loc.substring(loc.indexOf('?') + 1, loc.length());
                                httpRes.setHeader(nv.getName(), httpRes.encodeURL(url) + "?" + query);
                            } else {
                                httpRes.setHeader(nv.getName(), httpRes.encodeURL(nv.getValue()));
                            }
                        } else if (nv.getName().equalsIgnoreCase("content-length")) {
                            //ignore: nbrif case returns wrong content-length
                        } else {
                            httpRes.setHeader(nv.getName(), nv.getValue());
                        }
                    }
                }

                String replaced = replaceFormValues(tm.getMarkup(), httpReq);
                if (this.debug) {
                    LOG.debug("REPLACED: \n" + replaced);
                }

                httpRes.getWriter().print(replaced);
                if (this.debug) {
                    LOG.debug("processTranscodeMarkup: END");
                }
            } else {
                httpRes.setStatus(500);
                httpRes.getWriter().print("Transcoding error: " + tm.getResultCode() + " " + tm.getResultMessage());
                LOG.debug("processTranscodeMarkup: Transcoding error - " + tm.getResultCode() + " " + tm.getResultMessage());
            }

        } catch (TranscodingException e) {
            httpRes.setStatus(500);
            httpRes.getWriter().print(e.getMessage());
            LOG.error("Error by transcode markup.", e);
        }

    }

    protected void processTranscodeImage(ProxyConfig config, HttpServletRequest httpReq, HttpServletResponse httpRes)
            throws IOException {

        String strUrl = httpReq.getParameter("url");
        if (strUrl == null) {
            strUrl = "";
        } else {
			strUrl = replaceUrlParametersForIC(strUrl);
		}
        if (strUrl.length() == 0) {
            httpRes.setStatus(404);
            return;
        }

        String sourceContentType = httpReq.getParameter("format");
        byte[] sourceimage = null;
        if (strUrl.startsWith("http")) {
            // get remote file
            HttpClient httpclient = new DefaultHttpClient();
            HttpParams params = httpclient.getParams();
            HttpConnectionParams.setConnectionTimeout(params, 5000);
            HttpConnectionParams.setSoTimeout(params, 5000);

            try {
			HttpGet httpget = new HttpGet(strUrl);
			HttpResponse response = httpclient.execute(httpget);
            HttpEntity entity = response.getEntity();

            if (entity != null) {
                sourceimage = EntityUtils.toByteArray(entity);
                entity.consumeContent();
            } else {
                httpRes.setStatus(404);
                return;
            }
			} catch (Throwable t) {
				LOG.error("URL producing error is: "+strUrl, t);
			}
            if (sourceimage == null) {
                httpRes.setStatus(404);
                return;
            }
        } else {

            File f = new File(context.getRealPath(strUrl));

            if (f.isDirectory()) {
                httpRes.setStatus(404);
                return;
            }
            if (!(f.exists())) {
                httpRes.setStatus(404);
                return;
            }
            FileInputStream fis = new FileInputStream(f);

            // Create the byte array to hold the data
            sourceimage = new byte[(int) f.length()];
            // Read in the bytes
            int offset = 0;
            int numRead = 0;
            while ((offset < sourceimage.length)
                    && ((numRead = fis.read(sourceimage, offset, sourceimage.length - offset)) >= 0)) {
                offset += numRead;

            }
            fis.close();
        }

        try {
            TranscodeImage ti = new TranscodeImage(config, sourceimage, sourceContentType);

            if (ti.getResultCode() == 0) {
                httpRes.setContentType(ti.getContentType());
                httpRes.setContentLength(ti.getContentLenght());
                httpRes.setStatus(ti.getHttpResultCode());

                NameValue[] headers = ti.getResponseHeaders();
                if (headers != null) {
                    for (int i = 0; i < headers.length; i++) {
                        NameValue nv = headers[i];
                        httpRes.setHeader(nv.getName(), nv.getValue());
                    }
                }
                httpRes.getOutputStream().write(ti.getImage());
            } else {
                httpRes.setStatus(500);
                httpRes.getWriter().print("Transcode image error: " + ti.getResultCode() + " " + ti.getResultMessage());
            }

        } catch (TranscodeImageException e) {
            httpRes.setStatus(500);
            httpRes.getWriter().print(e.getMessage());
            LOG.error("Error by transcode image.", e);
        }
    }

    protected void processInternalResource(ProxyConfig config, HttpServletRequest httpReq, HttpServletResponse httpRes)
            throws IOException {

        String uri = config.getRelUri();
        if (uri == null) {
            uri = "";
        }
        int start = uri.lastIndexOf('/') + 1;
        if (start < 0) {
            start = 0;
        }
        if (uri.length() == 0) {
            httpRes.setStatus(404);
            return;
        }

        String resourceName = uri.substring(start, uri.length());

        try {
            Date modsincedate = null;
            if (httpReq.getHeader("If-Modified-Since") != null) {
                SimpleDateFormat modsince = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.ENGLISH);
                try {
                    modsince.parse(httpReq.getHeader("If-Modified-Since"));
                    modsincedate = modsince.getCalendar().getTime();
                } catch (ParseException e) {
                    LOG.error("Error by parse modsince.", e);
                }
            }

            InternalResource ir = new InternalResource(proxyUrl, wsUser, wsPassword, resourceName, modsincedate);

            if (ir.getResultCode() == 0) {
                httpRes.setContentType(ir.getContentType());
                httpRes.setCharacterEncoding(ir.getCharacterEncoding());
                httpRes.setContentLength((int) ir.getContentLenght());
                // enable client cache for internal resources
                httpRes.addHeader("Cache-Control", "max-age=3600, must-revalidate");
                SimpleDateFormat d = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss z", Locale.ENGLISH);
                d.setTimeZone(TimeZone.getTimeZone("GMT"));
                Calendar calendar = Calendar.getInstance();
                calendar.setTimeZone(TimeZone.getTimeZone("GMT"));
                httpRes.addHeader("Last-Modified", d.format(calendar.getTime()));

                NameValue[] headers = ir.getResponseHeaders();
                if (headers != null) {
                    for (int i = 0; i < headers.length; i++) {
                        NameValue nv = headers[i];
                        httpRes.addHeader(nv.getName(), nv.getValue());
                    }
                }

                httpRes.getOutputStream().write(ir.getData());

            } else if (ir.getResultCode() == 2) {
                // resource not modified
                httpRes.setStatus(304);
            } else {
                httpRes.setStatus(500);
                httpRes.getWriter().print("InternalResource error: " + ir.getResultCode() + " " + ir.getResultMessage());
            }

        } catch (InternalResourceException e) {
            httpRes.setStatus(500);
            httpRes.getWriter().print(e.getMessage());
            LOG.error("Error by process InternalResource.", e);
        }

    }

    private void processDeviceDetection(ProxyConfig config, HttpServletRequest httpReq) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("processDeviceDetection...");
        }
        try {
            DeviceDetection d = new DeviceDetection(config, this.deviceParamNames);
            if (d.getResultCode() == 0) {
                if (d.getDeviceParams() != null && d.getDeviceParams().length > 0) {
                    for (int i = 0; i < d.getDeviceParams().length; i++) {
                        NameValue n = d.getDeviceParams()[i];
                        httpReq.getSession().setAttribute(n.getName(), n.getValue());
                        if (LOG.isDebugEnabled()) {
                            LOG.debug(n.getName() + " = " + n.getValue());
                        }
                    }
                }
            }

        } catch (DetectionException e) {
            LOG.error("Error by Detection.", e);
        }
    }

    protected void processDeviceParams(ProxyConfig config, HttpServletRequest httpReq) {
        if (LOG.isDebugEnabled()) {
            LOG.debug("processDeviceParams...");
        }
        // this is an optimization to call getDeviceParams only once per session
        // we can't use httpReq.getSession().isNew() because session might already be initialized by detection workflow


        if (httpReq.getSession().getAttribute("UAID") == null) {
            processDeviceDetection(config, httpReq);
        } else { // update values from JavaScript display detection
            String width = (String) httpReq.getSession().getAttribute("ImageMaxWidth");
            String height = (String) httpReq.getSession().getAttribute("ImageMaxHeight");
            if (width != null || height != null) {
                String emvAdValue;
                String emvccCookie = getCookieValue(httpReq.getCookies(), "emvcc");
                if (emvccCookie != null) {
                    emvAdValue = getCookieValue(httpReq.getCookies(), "emvAD");
                } else {
                    emvAdValue = httpReq.getParameter("emvAD");
                }
                if (emvAdValue != null && emvAdValue.length() > 0 && !"0x0".equals(emvAdValue)) {
                    httpReq.getSession().setAttribute("ImageMaxWidth", emvAdValue.substring(0, emvAdValue.indexOf('x')));
                    httpReq.getSession().setAttribute("ImageMaxHeight", emvAdValue.substring(emvAdValue.indexOf('x') + 1,
                            emvAdValue.length()));
                }
            }
        }
    }

    private static String getCookieValue(Cookie[] cookies, String cookieName) {
        if (cookies != null) {
            for (int i = 0; i < cookies.length; i++) {
                Cookie cookie = cookies[i];
                if (cookieName.equals(cookie.getName())) {
                    return (cookie.getValue());
                }
            }
        }
        return null;
    }

    private String replaceFormValues(String markup, HttpServletRequest httpReq) {
        /*firstname,surname,id,addressline1,addressline2,suburb,city,postalcode,cellphone*/
        if ("POST".equalsIgnoreCase(httpReq.getMethod())) {
            if (this.debug) {
                LOG.debug("replaceFormValues NOT REPLACED: \n" + markup);
            }
            LazyValidatorForm form = (LazyValidatorForm) httpReq.getSession(true).getAttribute("applyForm");
            if (form instanceof LazyValidatorForm && !form.getMap().isEmpty()) {
                @SuppressWarnings("unchecked")
                Set<Map.Entry<String, Object>> entrySet = form.getMap().entrySet();
                for (Map.Entry<String, Object> entry : entrySet) {
                    markup = markup.replaceAll("@" + entry.getKey(), (String) entry.getValue());
                }
            }
        }
        if (this.debug) {
            LOG.debug("replaceFormValues is not a POST request");
        }
        return markup;
    }

	private String replaceUrlParametersForIC(String url) {
		String result = url;
		result = result.replace(" ", "%20").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
		//TODO: add problematical characters here
		return result;
	}
}
